//
//  HQCapture9ViewController.m
//  精通Objective-C
//
//  Created by 花强 on 2017/12/13.
//  Copyright © 2017年 花强. All rights reserved.
//

#import "HQCapture9ViewController.h"
#import "C9BasicGreeter.h"
#import "C9RunningWidget.h"

#import "C7Calculator.h"
#import "AuditingInvoker.h"
#import "AspectProxy.h"

@interface HQCapture9ViewController ()

@end

@implementation HQCapture9ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    /*
     
     第九章 专家级技巧：使用运行时系统API
     
     第7章和第8章介绍了OC的动态特性和用于实现这些特性的运行时系统结构；
     本章将通过几个示例程序使你获得使用运行时系统功能及其API的实践经验；
        你将使用NSInvocation API创建一个动态代理；
        使用NSBundle API动态加载你编写的框架包；
        还将编写一个广泛使用运行时系统库API的程序；
     本章之后，您将能够掌握错综复杂的OC运行时系统；
     
     是不是很期待；
     
     9.1 使用可选包扩展程序
     
     OC提供了非常多的动态特性，他们提供了这门语言的能力和灵活性；使你能够在执行程序的过程中修改程序；
        第七章介绍了动态加载，还简要介绍了使用Foundation框架中的NSBundle类管理包的方式；
        接下来我们将使用这些特性编写一个程序，使用可选包扩展正在运行的程序；
     
     9.1.1 方法
     
     这个例子实际需要编写两个工程；
        需要在一个工程中编写可以使用可选包的程序；
        在另一个工程中创建可选包；
     
     具体步骤如下：
     1）在工程1中，编写一个协议和遵循该协议的类；
     2）在工程2中，创建一个包含另一个遵守该协议的类的可选包；
     3）在工程1中，使用NSBundle类找到并加载这个包（在步骤2中创建的），使用该包中的类创建一个对象，然后调用这个对象中的方法；
     
     不是很难理解，我们继续看；
     
     9.1.2 步骤1：编写基础代码
     
     工程1我们就是用当前的这个项目；
     接下来创建一个协议和遵守这个协议的类；
        我们已经知道（第二章）使用协议可以声明通用的方法和属性；
        将协议与动态类型一起使用可以获得一种理想的机制；
            通过包以动态方式加载的类设置实现的方法；
        接下来新建这个协议；
            选择协议模板，这样就在导航栏窗格中添加了一个名为C9Greeter.h的头文件；
            点击可以看到这个C9Greeter协议；之后添加一个方法；
        再建一个遵循这个协议的类C9BasicGreeter；
     （Code9_1：C9Greeter.h C9BasicGreeter.h C9BasicGreeter.m）
     
     现在我们来进行测试这段代码；
        导入C9BasicGreeter类的头文件；
        实现如下代码；
     */
    id<C9Greeter> greeter1 = [[C9BasicGreeter alloc] init];
    NSLog(@"%@",[greeter1 greeting:@"Hello"]);

     /*
      log:
      2017-12-15 15:06:30.574490+0800 精通Objective-C[38928:3729761] Hello,Wrold
      
      我们慢下脚步，来看看这段代码：
        首先，在遵守协议的时候，我们并没有通过扩展的方式，声明当前类已经遵循了这个协议；
            @interface C9BasicGreeter()<C9Greeter>
            @end
        而是手动将这个协议的方法在C9BasicGreeter类中进行了实现；
        这个里创建的BasicGreeter对象赋给了一个名为greeter的变量；
        这个变量被声明为id类型并且遵守Greeter协议；
        然后，调用了对象中的方法greeting：，最后显示了结果；
      
      使用id<C9Greeter> greeter1声明的变量，就可以将任何遵守C9Greeter协议的OC对象赋值给他；
      
      接下来要创建另一个遵守C9Greeter协议的类，只不过这次是在可选包中创建；
      
     9.1.3 步骤2：创建一个可选包
      
      新建一个工程；参考下图：
      （Pic9_1）
      
      命名为C9CustomGreeter；这样我们就创建了一个空包；
      
      我们来看下这个工程的导航栏窗格，如下图：
      （Pic9_2）
      1）products分组下边有一个.bundle文件，这个就是要添加到你编写的代码和资源中的包；
      2）info.plist也是一个必不可少的文件，含有这个包的配置信息；
      
      接下来向该包中添加一个实现C9Greeter协议的类；
        要做到这一点，首先需要将协议对应的头文件包含到这儿工程中；
        我们按住Control点选导航栏窗格，xcode会打开一个下拉菜单，我们可以添加文件到当前工程；（如下图）
      （Pic9_3）
      （Pic9_4）
        继续我们的操作，在可选包工程中添加一个遵守C9Greeter协议的新类：C9CustomGreeter；
      （Code9_2：C9CustomGreeter.h C9CustomGreeter.m）
      
      通过Product菜单中选择Build选项，编译了这个包；
        这样我们就创建了一个包；
        选择这个包文件，可以在右侧文件检查器显示出完整的文件路径；
        当你使用NSBundle类加载这个包的时候，需要用到这个路径；

     9.1.4 步骤3：动态加载包
      
      接下来我们需要通过该包以动态的方式创建一个对象（通过该包中的类），并调用这个对象的方法；
      
      在实际测试代码运行是前，我们还需要做一些准备：
        最主要的就是如何获取bundle包的地址;
        我们将通过main函数的参数argv[]，来获取对应传入的参数（这个可以参考我的C语言blog中关于这部分的讲解），稍后会有介绍；
        复制包路径，按照下图在工程1中进行配置：
        （Pic9_6）
        同时在工程目录下新建一个plist文件，用来记录和保存这个路径参数；(其他的保存路径方式均可)
        （Code_main.m）
      
      接下来，回到当前工程（工程1），执行如下代码；
      */
    id greeter = [[C9BasicGreeter alloc] init];
    NSLog(@"%@",[greeter greeting:@"Hello-1"]);
    
    id greeterB;
    
    NSString * filePath = [[NSBundle mainBundle]pathForResource:@"PathList" ofType:@"plist"];
    NSDictionary * dic = [NSDictionary dictionaryWithContentsOfFile:filePath];
    NSLog(@"%@",dic);
    NSString * bundlePath =dic[@"bundleGreeterPath"];
    
    NSBundle * greeterBundle = [NSBundle bundleWithPath:bundlePath];
    
    if (greeterBundle == nil) {
        NSLog(@"Bundle not found at this path!");
    }else{
        NSError * error;
        BOOL isLoaded = [greeterBundle loadAndReturnError:&error];
        if (!isLoaded) {
            NSLog(@"Error = %@",[error localizedDescription]);
        }else{
            //加载包后使用该包创建一个对象并向这个对象发送一条消息
            Class greeterClass = [greeterBundle classNamed:@"C9CustomGreeter"];
            greeterB = [[greeterClass alloc] init];
            NSLog(@"%@",[greeterB greeting:@"Hello-2"]);
            
            //使用完以动态方式加载的包，现在卸载它
            //先释放所有用包中类创建的对象！
            greeterB = nil;
            BOOL isUnloaded = [greeterBundle unload];
            if (!isUnloaded) {
                NSLog(@"Counld not unload bundle!");
            }
        }
    }
      /*
    这段代码虽然看起来很长，我们来分段理解下：
       1）获取包的路径参数；
       2）创建一个NSBundle对象；
       3）加载包；
       4）获得包中的类；
       5）卸载包；
       6）设置包路径参数；
       
    再逐条研究下这些代码的逻辑：
       1.获取包的路径参数
       OC中main()函数会设置两个参数：argc和argv；
       使用这些参数能够在运行程序的过程中向程序传递参数；
       int main(int argc, char * argv[]);
        参数argc设置了程序参数的数量；
        argc数组存储了以空格分隔的参数值；
            argv[0]存储的是程序的名字，对应的argc的值大于等于1；（如果实在命令行中执行程序 程序名以及其之后空格间隔的内容就是main函数接受的参数，这一部分在我的C语言blog中有详解）；
       我们刚刚在scheme中添加argument就是工程2中生成的包的路径；
       这样这一点对应的代码就好理解了；

       2.创建一个NSBundle对象
       NSBundle * greeterBundle = [NSBundle bundleWithPath:bundlePath];
       if (greeterBundle == nil) {
       }
       根据路径创建一个NSBundle对象，并验证该对象是否成功创建；
       
       3.加载包
       -(BOOL)loadAndReturnError:(NSError **)error;
       NSBundle提供了多个向OC运行时系统中加载包的方法，上边的就是其中之一，出错的话也会及时返回，成功则返回BOOL值YES；
       
       4.获得包中的类
       从包中获取类，然后创建一个对象使用；
       这里也可以使用[greeterBundle principalClass]的方式获取类greeterClass；
            principalClass被用于获取包的首要类；
            首要类用于控制包中含有的其他类；
            包的Info.plist文件舍之类首要类的名字（没有设置的话，那么从包中加载的第一个类就是首要类）；
       （Pic9_7）
            这个生成的实例被赋值给了已经声明的变量greeterB，这个变量在声明时最好直接声明为id<C9Greeter> greeterB;

       5.卸载包
       不再需要的包可以卸载，以节省系统资源；
       
       6.设置包路径参数
       其实要做的就是查明bundle包的路径，在程序运行时将这个路径设置为它的输入参数；
       如何查找该路径以及如何设置，前面我们都已截图说明过，这里不再赘述；
       
       这里有必要知道，scheme方案定义了Xcode功能和工作区的创建和测试设置；
       创建配置信息中含有执行目标程序时传递的参数；因此在工程1中这里设置的是包的完整路径；
       
       说了这么多一直还没给大家看log呢；我们来运行下看看：
       2017-12-18 14:16:24.005835+0800 精通Objective-C[50675:5339959] Error = The bundle “C9CustomGreeter” couldn’t be loaded because it is damaged or missing necessary resources.
       报错了！
       这个问题我大致查了一下，应该是涉及到动态加载更新代码的方式，在客户端是受限的，或者有其他方式；
            比如我们将一个模块独立成一个包，然后从服务器下载，再到客户端去使用，会达到动态更新的目的；
            这个问题，暂时还没有更加深入的理解，也在网上同人交流，mark在这，日后清楚之后，再分出一节单独讨论吧；（本段标红）
       
       为了证明这种方式的可行性，我们新建了一个项目C9DynaLoader，将其作为工程1的替代，将协议和实现协议的类都在这个工程中使用；
       在其main.m中执行上述逻辑的测试代码如下：
       （Code_C9DynaLoader_main.m）
       
       目录结构截图署截图说明如下：
       （Pic9_8）（Pic9_9）

       和原来的工程1一样，还是要设置包路径参数；
       运行C9DynaLoader，log如下：
       2017-12-18 15:11:32.521433+0800 C9DynaLoader[51745:5414801] Hello, World!
       2017-12-18 15:11:32.521733+0800 C9DynaLoader[51745:5414801] Hello-1,Wrold
       2017-12-18 15:11:32.530178+0800 C9DynaLoader[51745:5414801] Hello-2,Universe
       
       至此，我们看到：
        greeterB对象是通过以动态方式加载的C9CustomGreeter包创建的；这就是通过使用NSBundle类以动态的方式加载包，向正在运行的程序添加代码和资源的方式；
       
       
       ========
       
     9.2 使用运行时系统API
       
     接下来仍然是编写一段程序：
       该程序会使用运行时系统API以动态的方式创建一个类和一个类实例；
       然后以动态方式向该实例添加一个变量；
       
       创建类RunningWidget用于说明；
            在类中添加相应代码；(部分内容可参考第7章)
        （Code_RunningWidget.m）
       */
    C9RunningWidget * test = [[C9RunningWidget alloc] init];
    [test test9_2];
    /*
     log:
     2017-12-18 17:07:40.425307+0800 精通Objective-C[53341:5496447] W height = 15
     2017-12-18 17:07:40.425491+0800 精通Objective-C[53341:5496447] Invoking method with selector display on C9Widget instance
     2017-12-18 17:07:40.425634+0800 精通Objective-C[53341:5496447] W width = 10
     
     从功能上讲，这段代码执行了如下操作：
     1）定义了一个方法的实现参数；
     2）创建并注册了一个类；
     3）创建了一个类实例；
     4）以动态方式向该实例添加一个变量；
     
     我来逐个分析下；
     
     9.2.1 定义方法的实现函数
     
     OC的方法就是一个至少接收两个陈述（self和_cmd）的C语言参数；（第8章）
     
     9.2.2 创建并注册类
     
     要使用运行时系统API以动态的方式创建类，必须执行下列步骤：
     1）新建一个类及其元类；
     2）向这个类添加方法和实例变量（如果有的话）；
     3）注册新建的类；
     
     运行时系统桉树class_addMethod参数：
        应该添加方法的类；
        设置被添加方法名称的选择器；
        实现该方法的函数；
        类型编码（描述方法的参数类型和返回值类型的字符串）；
     
     类型编码：
        字符v代表void；
        字符@代表SEL类型（https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html#//apple_ref/doc/uid/TP40008048-CH100）；
     
     这里附上一张截图Objective-C type encodings：
     （Pic9_1）
     
     class_addMethod函数的类型参数中的编码必须以固定顺序排列：
        第一个编码用于设置返回值类型；
        第二个编码用于设置方法的隐式参数self（类型为id）；
        第三个编码用于设置方法的隐式参数_cmd（类型为SEL）；
        其他编码用于设置方法的显式参数的类型；
     
     对应上边的这个表，就好理解各个编码的含义了；
     
     9.2.3 创建类实例
     
     创建一个widget实例并设置实例变量的值，然后调用了实例中的方法；
     
     9.2.4 以动态的方式向类实例添加变量
     
     OC没有提供向对象添加实例变量的功能；
     然而，使用运行时系统的关联对象特性，可以高效地模拟这个功能；
     
     关联对象：
        指通过关键字引用的 附加到类实例中的对象；
        我们可以在，不允许使用是实例变量的OC分类中使用关联对象；
        在创建关联对象时，需要设置关联对象的关键字、关联对象的内存管理策略和它的值；
            NSNumber * width = [NSNumber numberWithInteger:10];
            objc_setAssociatedObject(widget, @"width", width, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
     
            id result = objc_getAssociatedObject(widget, @"width");

     运行时系统API含有一个枚举型值，其中含有内存管理策略的所有可能值；
        OBJC_ASSOCIATION_RETAIN_NONATOMIC：向关联对象分配非原子性的强引用；（和nonatomic和strong特性的属性类似）
     
     本示例展示了使用运行时系统API以动态的方式创建类、类实例和关联对象的方式；
     参阅Apple objective-c runtime programming guide（https://developer.apple.com/documentation/objectivec/objective_c_runtime?language=objc），可以学习更多；
     
     9.3 创建动态代理
     
     本节将实现本章的最后一个示例程序：
        如何使用Foundation框架中的NSInvocation和NSProxy类，实现动态消息转发功能；
     
     OC提供了多个消息转发选项（第三章）：
        使用NSObject类的forwardingTargetForSelector:方法实现的快速转发；
        以及使用NSObject类的forwardInvocation:方法实现标准（完全）转发；
     
     标准转发的一大优点是使程序能够对对象的消息、参数和返回值执行额外的处理操作；
     
     将该功能与NSProxy类一起使用，可以在OC中获得不错的面向切面编程（AOP）机制；
        AOP是一种编程范式，其目的是通过横切（cross-cutting）将（依赖或影响程序的其他部分）功能与程序中的其他组成部分分隔开；提高程序的模块化程度；
        NSProxy是Foundation框架中的一个类，专门用于创建代理，其作用就像真正类的接口；
     
     下面我们通过创建一个NSProxy类的子类并实现forwardInvocation：方法：
        从而使消息能够发送到应用了横切功能的真正类；
     
     程序的结构可以是这样的：
     
     协议 Invoker               根类NSProxy（forwardInvocation：、methodSignatureForSelector：）
     |                                ^
     |限定                            |
     |                                |         代理
     ——> AuditingInvoker <—— AspectProxy ————> RealSubject
                                      ^
                                      |
                                      ————> NSValue（（SEL）选择器）
     （Pic9_2）
     
     接下来分步理解下，先从Invoker协议开始；
     
     9.3.1 创建Invoker协议
     
     我们新建了一个文件夹9.3AspectProxy，用于存放这里的类；
     
     首先创建一个协议和一个遵守该协议的类；
        选择OC的协议模板，命名为Invoker；添加必要的代码；
        （Code_Invoker.h）

     临时对代理/协议有了点理解；
        在一个类M中导入一个协议P，然后在这个类的.m中实现该协议方法；
        M.h中的接口就被协议P扩展了，但是用来扩展接口的协议P只是提供了接口，其实现则需要由被扩展了的M在其.m中进行实现；
        这样通过这种接口扩展的方式，我们可以给不同继承体系的类提供统一的行为；
     
     我们来看一下这个协议中定义的两个接口；
        -(void)preInvoke:(NSInvocation *)inv withTarget:(id)target；
            这是一个必要方法，所有遵循该协议的类都必须实现这个方法；
            能够在调用对象中的方法前执行对功能的横切；
        -(void)postInvoke:(NSInvovation *)inv withTarget:(id)target;
            是一个可选方法；
            能够在调用对象中的方法后执行对功能的横切；
     
     接着我们编写一个遵循该协议的类AuditingInvoker，实现横切功能；
     （Code_AuditingInvoker.h,AuditingInvoker.m）
     
     实现了横切功能之后，我们再来处理下NSproxy子类；
     
     9.3.2 编写代理类
     
     下面创建的代理类，它是NSProxy的子类；
        它会实现消息转发方法forwardInvocation：和methodSignatureForSelector：；
        我们来新建这个类AspectProxy；继承自NSProxy；
     （Code_AspectProxy.h,AspectProxy.m）
     
     来看一下这段代码：
        初始化方法初始化了相应的AspectProxy对象的实例；
        注意，因为NSProxy是一个基类，所以这些方法的开头没有调用[super init];
        当调用目标方法的选择器与在AspectProxy对象中注册的选择器匹配时，forwardInvocation：方法会调用目标对象中的方法，并根据条件语句的判断结果调用AOP功能；
     
     现在再回看之前的Pic9_2，是不是就清晰了；
     
     最终向AspectProxy代理对象发送原对象可接收的消息，而不是直接向原对象发送消息；
     我们来测试下；
     
     9.3.3 测试AspectProsy程序
     
     我们是用第七章的C7Calculator类（还记得动态方法决议不？）来进行测试；
        该类声明了下列方法：
        -(NSNumber *)sumAddend1:(NSNumber *)adder1 addend2:(NSNumber *)adder2;
        -(NSNumber *)sumAddend1:(NSNumber *)adder1 :(NSNumber *)adder2;//这个方法的第二个参数使用空参数
*/
    //创建对象
    id calculator = [[C7Calculator alloc] init];
    NSNumber * addend1 = [NSNumber numberWithInteger:-1];
    NSNumber * addend2 = [NSNumber numberWithInteger:2];
    NSNumber * addend3 = [NSNumber numberWithInteger:5];
    
    //为该对象创建代理
    NSValue * selValue1 = [NSValue valueWithPointer:@selector(sumAddend1:addend2:)];
    NSArray * selValues = @[selValue1];
    AuditingInvoker * invoker = [[AuditingInvoker alloc] init];
    
    id calculatorProxy = [[AspectProxy alloc] initWithObject:calculator selectors:selValues andInvoker:invoker];
    
    //使用指定的选择器向该代理发送消息
    [calculatorProxy sumAddend1:addend1 addend2:addend2];
    //使用没有特殊处理的其他选择器向该代理发送消息
    [calculatorProxy sumAddend1:addend2 :addend3];
    //为这个代理注册另一个选择器并再次向其发送消息
    [calculatorProxy registerSelector:@selector(sumAddend1::)];
    [calculatorProxy sumAddend1:addend2 :addend3];

    /*
   log:
   2017-12-21 11:42:53.211276+0800 精通Objective-C[83628:6659416] Creating audit log before sending message with selector sumAddend1:addend2: to C7Calculator object
   2017-12-21 11:42:53.211419+0800 精通Objective-C[83628:6659416] Invoking method on C7Calculator object with selector sumAddend1:addend2:
   2017-12-21 11:42:53.257332+0800 精通Objective-C[83628:6659416] Creating audit log after sending message with selector sumAddend1:addend2: to C7Calculator object
   2017-12-21 11:42:53.257634+0800 精通Objective-C[83628:6659416] Invoking method on C7Calculator object with selector sumAddend1::
   2017-12-21 11:42:53.257853+0800 精通Objective-C[83628:6659416] Creating audit log before sending message with selector sumAddend1:: to C7Calculator object
   2017-12-21 11:42:53.258002+0800 精通Objective-C[83628:6659416] Invoking method on C7Calculator object with selector sumAddend1::
   2017-12-21 11:42:53.258146+0800 精通Objective-C[83628:6659416] Creating audit log after sending message with selector sumAddend1:: to C7Calculator object
   
   分析：
        调用C7Calculator对象的代理中的sumAddend1:addend2:方法时；
            会调用AuditingInvoker对象中的preInvoker：，postInvoker：方法；
            还会调用真正目标（C7Calculator对象）中的sumAddend1:addend2:方法；
        方法通过AspectProxy代理进行注册，才会调用AuditingInvoker对象中的AOP方法；
        不注册，就不会调用；
   
     9.4 小结
     
     本章介绍了3个程序，我们使用OC运行时及其API完成了这些程序；
     还有创建可选包和使用Xcode将代码导入工程的方法；
     
     好好复习一下第二部分的内容，接下来将开始第三部分Foundation框架的介绍。
     
   
     
     */
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

/*
#pragma mark - Navigation

// In a storyboard-based application, you will often want to do a little preparation before navigation
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
    // Get the new view controller using [segue destinationViewController].
    // Pass the selected object to the new view controller.
}
*/

@end
